// TODO: Search function highlighting using <mark> tag.

package main

import (
    "./cache"
    "./helpers"
    "./query"
    "./userstat"
    "bytes"
    "config"
    "database/sql"
    "encoding/base64"
    "fmt"
    "html/template"
    "io"
    "log"
    "math"
    "net"
    "net/http"
    "net/http/fcgi"
    "os"
    "path/filepath"
    "sort"
    "strconv"
    "strings"
    "time"
    "yotsuba"
)

type unprocessedPage struct {
    Boards      []string
    Threads     []*yotsuba.SqlThread
    Pages       []int
    PageNo      int
    PageBase    string
    NearEnd     bool
    BoardName   string
    Expired     time.Time
    Title       string
    FullThread  bool
    Generated   time.Time
    ResultTotal int
    Search      map[string]string
    TimeElapsed int64
}

type processedPage struct {
    Content       *bytes.Reader
    LastGenerated time.Time
}

func getImage(bname string, id int) (*yotsuba.SqlImage, error) {
    i := new(yotsuba.SqlImage)

    row := query.Get(bname, "getImage").QueryRow(id)
    e := row.Scan(&i.Size, &i.FullX, &i.FullY, &i.ThumbX,
        &i.ThumbY, &i.ThumbFile, &i.ImgFile)
    if e != nil {
        return nil, e
    }
    return i, nil
}

func buildPost(b *config.Board, row *sql.Rows) (*yotsuba.SqlPost,
    *yotsuba.SqlThread,
    *sql.NullInt64) {

    p := new(yotsuba.SqlPost)
    t := new(yotsuba.SqlThread)
    var resultCount sql.NullInt64
    e := row.Scan(&p.IntId, &p.PostId, &p.GhostId, &p.Time, &p.Name, &p.Trip,
        &p.Subject, &p.Mail, &p.PosterUid, &p.Text, &p.Spoiler, &p.PostDeleted,
        &p.Parent, &p.ImgIntId, &p.ImgName, &p.ImgUrl, &p.Highlight, &p.Auth,
        &t.Sticky, &t.Locked, &t.Deleted, &t.Expired, &t.PostCount, &t.ImgCount,
        &t.ThreadUrl, &resultCount)
    if e != nil {
        log.Panic(e)
    }

    p.FmtClass = "post"
    if p.PostDeleted != nil {
        p.FmtPostDel = p.PostDeleted.Format(yotsuba.NihTime)
    }

    if p.GhostId.Valid {
        p.FmtPostNo = fmt.Sprintf("%d,%d", p.PostId.Int64, p.GhostId.Int64)
        p.FmtClass += " ghost"
    } else {
        p.FmtPostNo = strconv.FormatInt(p.PostId.Int64, 10)
    }

    if p.Highlight.Bool {
        p.FmtClass += " highlight"
    }
    p.FmtTime = p.Time.Format(yotsuba.NihTime)
    p.EscapedBody = template.HTML(
        yotsuba.BBtoHTML(
            p.Text.String, b.Abbrev, p.Parent.Int64))

    // Get image if present.
    if p.ImgIntId.Valid {
        id := int(p.ImgIntId.Int64)
        i, e := getImage(b.TableName, id)
        if e != nil {
            // Not necessarily an error, image may have been reported.
            p.Img = nil
            p.ImgIntId = sql.NullInt64{0, false}
        } else {
            p.Img = i
        }
        p.ImgName.String = truncateFilename(p.ImgName.String, b.ImgNameCutoff)

        urlBytes := make([]byte, len(p.ImgUrl.String))
        copy(urlBytes, p.ImgUrl.String)
        p.FmtImgUrl = base64.URLEncoding.EncodeToString(urlBytes)
    }

    return p, t, &resultCount
}

func truncateFilename(filename string, cutoff int) string {
    ext := filepath.Ext(filename)
    if len(filename) > cutoff {
        filename = filename[:cutoff-len(ext)] + "(...)" + ext // truncate image name
    }
    return filename
}

func pageRange(total int64, itemsPerPage, pagesToShow, pageNo int) []int {
    maxPageNo := total / int64(itemsPerPage)
    lower := int(math.Max(0.0,
        float64(pageNo-(pagesToShow/2))))
    upper := int(math.Min(float64(maxPageNo),
        float64(lower+pagesToShow)))
    pageRange := make([]int, 0, upper-lower)
    for i := lower; i <= upper; i++ {
        pageRange = append(pageRange, i)
    }
    return pageRange
}

// utility function for checking unmanagable errors.
func chk(e error) {
    if e != nil {
        log.Panic(e)
    }
}

type boardPage func(w http.ResponseWriter, r *http.Request,
    t *template.Template, b *config.Board)

type cachedPage func(w http.ResponseWriter, r *http.Request,
    t *template.Template, b *config.Board) (cache.PageBuilder, time.Duration)

// wrapper to share the db handle and templates with all cgi handlers which 
// need them.
func withBoard(f boardPage, t *template.Template,
    b *config.Board) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {
        if !userstat.LockUser(r.RemoteAddr) {
            return
        }
        defer userstat.UnlockUser(r.RemoteAddr)
        f(w, r, t, b)
    }
}

// wrapper for cacheable pages, also passes on board data like withBoard
func withCache(f cachedPage, t *template.Template,
    b *config.Board) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {
        if !userstat.LockUser(r.RemoteAddr) {
            return
        }
        defer userstat.UnlockUser(r.RemoteAddr)
        uri := strings.SplitAfterN(r.URL.Path, "#", 2)[0] //remove anchors.
        uri = uri + r.URL.RawQuery
        log.Println(uri + " requested")
        if page := cache.Fetch(uri); page != nil {
            page.Seek(0, 0)
            io.Copy(w, page)
            return
        }
        builder, lifetime := f(w, r, t, b)
        if builder == nil {
            return
        }
        cache.Render(w, builder, uri, lifetime)
    }
}

func main() {
    const sockPath = "/tmp/nih.socket"
    cfg := config.Defaults

    // Set up unix socket for communication with nginx.
    os.Remove(sockPath)
    uSock, err := net.ResolveUnixAddr("unix", sockPath)
    chk(err)
    listener, err := net.ListenUnix("unix", uSock)
    chk(err)
    os.Chmod(sockPath, 0666)

    initReportSystem()
    t := template.New("main")
    t.Funcs(template.FuncMap{"eq": helpers.Eq,
        "add": helpers.Add})
    _, err = t.ParseGlob(cfg.TemplatePath + "*.html")
    chk(err)

    // Install handlers for all configured boards; collect list of board names.
    type names struct {
        Names []string
    }
    boardNames := names{}

    for _, b := range config.Boards {
        http.HandleFunc("/"+b.Abbrev+"/", withCache(boardTop, t, b))
        http.HandleFunc("/"+b.Abbrev+"/res/", withCache(fullThread, t, b))
        http.HandleFunc("/"+b.Abbrev+"/search", withCache(search, t, b))
        http.HandleFunc("/"+b.Abbrev+"/post", withBoard(post, t, b))
        http.HandleFunc("/"+b.Abbrev+"/report", withBoard(post, t, b))
        http.HandleFunc("/"+b.Abbrev+"/report_landing",
            withBoard(reportLanding, t, b))
        http.HandleFunc("/"+b.Abbrev+"/report_commit",
            withBoard(reportCommit, t, b))
        boardNames.Names = append(boardNames.Names, b.Abbrev)
    }
    sort.Strings(boardNames.Names)

    http.HandleFunc("/redirect/", redirect)

    // Pre-generate archive front page.
    frontPage := new(processedPage)
    content := new(bytes.Buffer)
    err = t.ExecuteTemplate(content, "front", boardNames)
    chk(err)
    frontPage.Content = bytes.NewReader(content.Bytes())
    frontPage.LastGenerated = time.Now()
    front := func(w http.ResponseWriter, r *http.Request) {
        page := frontPage.Content
        page.Seek(0, 0)
        _, err := io.Copy(w, page)
        chk(err)
        return
    }
    http.HandleFunc("/", front)

    fcgi.Serve(listener, nil)
}
